﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.IO;
using System.Reflection.Emit;
using msilgc.cbuilder.MSILSyntaxTree;

namespace msilgc.cbuilder.Reader
{
    internal class MsilReader {
        private static readonly IDictionary<short, OpCode> _instructionLookup;
        private static readonly object _syncObject = new object();
        private readonly BinaryReader _methodReader;
        private Instruction _current;

        private static int ToInt32(byte[] data)
        {
            return (data[0] << 0x0) | (data[1] << 0x8) | (data[2] << 0x10) | (data[3] << 0x18);
        }

        public static IEnumerable<Instruction> ReadAll(MethodInfo method) {
            MsilReader reader = new MsilReader(method);
            ICollection<Instruction> instructions = new List<Instruction>();
            while (reader.Read(method)) {
                instructions.Add(reader.Current);
            }
            return instructions;
        }

        static MsilReader() {
            if (_instructionLookup == null) {
                lock (_syncObject) {
                    if (_instructionLookup == null) {
                        _instructionLookup = GetLookupTable();
                    }
                }
            }
        }

        public MsilReader(MethodInfo method) {
            if (method == null) {
                throw new ArgumentException("method");
            }
            _methodReader = new BinaryReader(new MemoryStream(method.GetMethodBody().GetILAsByteArray()));
        }

        public Instruction Current {
            get {
                return _current;
            }
        }

        public bool Read(MethodInfo method) {
            long position = _methodReader.BaseStream.Position;
            if (_methodReader.BaseStream.Length == _methodReader.BaseStream.Position) {
                return false;
            }
            int instructionValue;
            if (_methodReader.BaseStream.Length - 1 == _methodReader.BaseStream.Position) {
                instructionValue = _methodReader.ReadByte();
            } else {
                instructionValue = _methodReader.ReadUInt16();
                if ((instructionValue & OpCodes.Prefix1.Value) != OpCodes.Prefix1.Value) {
                    instructionValue &= 0xff;
                    _methodReader.BaseStream.Position--;
                } else {
                    instructionValue = ((0xFF00 & instructionValue) >> 8) |
                                       ((0xFF & instructionValue) << 8);
                }
            }
            OpCode code;
            if (!_instructionLookup.TryGetValue((short)instructionValue, out code)) {
                throw new InvalidProgramException();
            }
            int dataSize = GetSize(code.OperandType);
            byte[] data = new byte[dataSize];
            _methodReader.Read(data, 0, dataSize);
            _current = new Instruction((int)position, code, data);
            int metadataToken;
            switch (code.OperandType)
            {
                case OperandType.InlineString:
                    _current.InlineString = method.Module.ResolveString(ToInt32(data));
                    break;
                case OperandType.InlineSwitch:
                    int nCases = ToInt32(data);
                    int[] targets = new int[nCases];
                    for (int i = 0; i < nCases; i++)
                    {
                        _methodReader.Read(data, 0, data.Length);
                        targets[i] = ToInt32(data);
                    }
                    _current.SwitchOffsets = targets;
                    break;
                case OperandType.InlineField:
                    metadataToken = ToInt32(data);
                    FieldInfo field = method.Module.ResolveField(metadataToken);
                    _current.FieldOperand = field;
                    break;
                case OperandType.InlineMethod:
                    metadataToken = ToInt32(data);
                    try
                    {
                        _current.MethodOperand = method.Module.ResolveMethod(metadataToken);
                    }
                    catch
                    {
                        _current.MemberOperand = method.Module.ResolveMember(metadataToken);
                    }
                    break;
                case OperandType.InlineType:
                    metadataToken = ToInt32(data);
                    _current.InlineType = method.Module.ResolveType(metadataToken);
                    break;
                /*metadataToken = ReadInt32(il, ref position);
                    // now we call the ResolveType always using the generic attributes type in order
                    // to support decompilation of generic methods and classes
                        
                    // thanks to the guys from code project who commented on this missing feature

                    instruction.Operand = module.ResolveType(metadataToken, this.mi.DeclaringType.GetGenericArguments(), this.mi.GetGenericArguments());
                    break;*/
                default:
                    break;
            }

            return true;
        }

        private static int GetSize(OperandType opType) {
            switch (opType) {
                case OperandType.InlineNone:
                    return 0;
                case OperandType.ShortInlineBrTarget:
                case OperandType.ShortInlineI:
                case OperandType.ShortInlineVar:
                    return 1;
                case OperandType.InlineVar:
                    return 2;
                case OperandType.InlineBrTarget:
                case OperandType.InlineField:
                case OperandType.InlineI:
                case OperandType.InlineMethod:
                case OperandType.InlineSig:
                case OperandType.InlineString:
                case OperandType.InlineSwitch:
                case OperandType.InlineTok:
                case OperandType.InlineType:
                case OperandType.ShortInlineR:
                    return 4;
                case OperandType.InlineI8:
                case OperandType.InlineR:
                    return 8;
                default:
                    return 0;
            }
        }

        private static IDictionary<short, OpCode> GetLookupTable() {
            IDictionary<short, OpCode> lookupTable = new Dictionary<short, OpCode>();
            FieldInfo[] fields = typeof(OpCodes).GetFields(BindingFlags.Static | BindingFlags.Public);
            foreach (FieldInfo field in fields) {
                OpCode code = (OpCode)field.GetValue(null);
                lookupTable.Add(code.Value, code);
            }
            return lookupTable;
        }
    }
}